package affd.logic;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

/**
 * A NameExtractor is a name detector which use specified word dictionary to extract
 * names from sentences.
 * 
 * @author Joteitti joteitti@cc.hut.fi
 *
 */
public class NameExtractor {
	
	public static float CASE_FACTOR = 0.05f;
	public static float SIMILARITY_TRESHOLD = 0.80f;
	public static float NORMALIZATION_TRESHOLD = 0.0f;
	
	protected HfstStarter hfstStarter;
	protected HashSet<String> deleteNames;
	
	/**
	 * Constructs a name extractor.
	 * 
	 * @param hfstStarter Language dictionary to detect names.
	 * @param deleteNames Names to be deleted.
	 */
	public NameExtractor(HfstStarter hfstStarter, HashSet<String> deleteNames) {
		this.hfstStarter = hfstStarter;
		this.deleteNames = deleteNames;
	}
	
	/**
	 * Extracts certain and uncertain names from the sentences.
	 * 
	 * @param sentences Sentences to be processed.
	 * @param uncertainNames Collection to be used for the uncertain names.
	 * @return Collection of the names.
	 */
	public HashMap<String, ArrayList<Token>> processSentences(ArrayList<Sentence> sentences) {
		
		HashMap<String, ArrayList<Token>> names = new HashMap<String, ArrayList<Token>>();	
		
		for (Sentence sentence : sentences) {
			ArrayList<Token> tokens = sentence.getTokens();
			for (Token token : tokens) {
				String content = token.getContent();
				Token.Type type = token.getType();
				
				// Get first letter's case
				boolean upperCase = Character.isUpperCase(content.charAt(0));
				
				// Skip white spaces and punctuation marks
				boolean skip = type == Token.Type.WHITE_SPACE || type == Token.Type.PUNCTUATION;
				
				if(!skip && (upperCase || deleteNames != null)) {
					ArrayList<String> normalized = hfstStarter.getBasicForms(content);
					
					// Find most similar basic form
					String normal = null;
					if(normalized != null) {
						float current = NORMALIZATION_TRESHOLD;
						for(String word : normalized) {
							float similarity = testSimilarity(content, word, CASE_FACTOR);
							if(similarity >= current && word!= null && !word.isEmpty()) {
								normal = word.toLowerCase();
								current = similarity;
							}
						}
					}
					
					// Check delete names list
					boolean onDeleteNames = false;
					if(deleteNames != null) {
						onDeleteNames = deleteNames.contains(content) || (normal != null && deleteNames.contains(normal));
					}
					
					// Check names list
					boolean onNames = false;
					if(normal != null) {
						ArrayList<Token> values = names.get(normal);
						if(values != null) {
							onNames = true;
							values.add(token);
						}
					} else {
						// Failed to found basic form so try find similar name
						ArrayList<Token> values = names.get(content);
						if(values != null) {
							onNames = true;
							values.add(token);
						} else {
							Set<String> keys = names.keySet();
							for(String key : keys) {
								values = names.get(key);
								for(Token value : values) {
									float similarity = testSimilarity(value.getContent(), content, CASE_FACTOR);
									if(similarity >= SIMILARITY_TRESHOLD) {
										onNames = true;
										values.add(token);
										break;
									}
								}
								
								if(onNames) {
									break;
								}
							}
						}
					}
					
					if(onNames) {
						// Name is already added to the list
					} else if(onDeleteNames) {
						ArrayList<Token> temp = new ArrayList<Token>();
						temp.add(token);
						
						// Add to names and use basic form as key if possible
						if(normal != null) {
							names.put(normal, temp);
						} else {
							names.put(content.toLowerCase(), temp);
						}
					} else if(upperCase && !hfstStarter.wasPronoun(content)) {
						ArrayList<Token> temp = new ArrayList<Token>();
						temp.add(token);
						
						// Add new name 
						if(normal != null) {
							names.put(normal, temp);
						} else {
							names.put(content.toLowerCase(), temp);
						}
					}
				}
			}
		}
		
		return names;
	}
	
	/**
	 * Very simple compare method to test two different strings.
	 * 
	 * @param string1 String to be tested.
	 * @param string2 String to be tested.
	 * @return Returns value from any negative value to 1.0.
	 */
	public static float testSimilarity(String string1, String string2, float caseFactor)
	{
		// Skip empty strings
		if(string1 == null || string2 == null || string1.isEmpty() || string2.isEmpty()) {
			return 0.0f;
		}
		
		char char1 = string1.charAt(0);
		char char2 = string2.charAt(0);
		
		// Check matching first letter
		if(Character.toLowerCase(char1) != Character.toLowerCase(char2)) {
			return 0.0f;
		}
			
		// Test first letter case
		boolean matchingCase = true;
		if(Character.isUpperCase(char1) != Character.isUpperCase(char2)) {
			matchingCase = false;
		}
		
		// To lower case
		string1 = string1.toLowerCase();
		string2 = string2.toLowerCase();
		
		// Calculate matching sequence length
		float averageLength = (float) (string1.length() + string2.length()) / 2.0f;
		float sequenceLength = 0.0f;
		for(int i = 0; i < Math.min(string1.length(), string2.length()); i++) {
			if(string1.charAt(i) != string2.charAt(i)) {
				break;
			}
			
			sequenceLength += 1.0f;
		}
		
		// TODO Merge with Levenshtein distance
		
		return (sequenceLength / averageLength) - (matchingCase ? 0.0f : caseFactor);
	}
}
